from sqlalchemy import select
from sqlalchemy.exc import DBAPIError, IntegrityError
from sqlalchemy.orm import joinedload

from poker_bot.core.exceptions.common import RepoError
from poker_bot.core.exceptions.user import (
    UserIdAlreadyExist,
    UserIdNotExist,
    UserTgIdAlreadyExist,
    UserTgIdNotExist,
)
from poker_bot.core.interfaces import UserReader, UserRepo
from poker_bot.core.models.dto import UserDTO
from poker_bot.core.models.dto.account import AccountDTO
from poker_bot.core.models.dto.user import UserWithDepositsDTO
from poker_bot.core.models.enums.user import UserRole
from poker_bot.database.converters.account import (
    convert_account_entity_to_db_model,
    convert_db_model_to_account_dto,
    convert_db_model_to_account_entity,
)
from poker_bot.database.converters.user import (
    convert_db_model_to_user_dto,
    convert_db_model_to_user_entity,
    convert_db_model_to_user_with_deposits_dto,
    convert_user_entity_to_db_model,
)
from poker_bot.database.exception_mapper import exception_mapper
from poker_bot.database.models import Account, User
from poker_bot.database.repositories.base import SQLAlchemyRepo
from poker_bot.domain.account import AccountEntity
from poker_bot.domain.user import UserEntity


class UserReaderImpl(SQLAlchemyRepo, UserReader):
    @exception_mapper
    async def get_user_by_id(self, user_id: int) -> UserWithDepositsDTO:
        """Получаем пользователя по id"""

        user: User | None = await self._session.scalar(
            select(User)
            .where(
                User.id == user_id,
            )
            .options(joinedload(User.deposits))
        )
        if not user:
            raise UserIdNotExist

        return convert_db_model_to_user_with_deposits_dto(user)

    @exception_mapper
    async def get_user_by_tg_id(self, user_tg_id: int) -> UserWithDepositsDTO:
        """Получаем пользователя по telegram id"""

        user: User | None = await self._session.scalar(
            select(User)
            .where(
                User.telegram_id == user_tg_id,
            )
            .options(joinedload(User.deposits))
        )
        if not user:
            raise UserTgIdNotExist

        return convert_db_model_to_user_with_deposits_dto(user)

    @exception_mapper
    async def get_users(self) -> list[UserDTO]:
        """Получаем пользователей"""

        res = await self._session.scalars(select(User))
        users: list[User] = list(res)

        return [convert_db_model_to_user_dto(user) for user in users]

    @exception_mapper
    async def get_user_accounts(self, user_tg_id: int) -> list[AccountDTO]:
        """Получаем аккаунты пользователя"""

        query = select(Account).join(Account.user).where(User.telegram_id == user_tg_id)

        res = await self._session.scalars(query)
        accounts: list[Account] = list(res)

        return [convert_db_model_to_account_dto(account) for account in accounts]

    @exception_mapper
    async def get_selected_user_account(self, user_tg_id: int) -> AccountDTO | None:
        """Получаем выбранный аккаунт пользователя"""

        account: Account | None = await self._session.scalar(
            select(Account)
            .join(Account.user)
            .where(
                Account.id == User.selected_account_id,
            )
        )
        if not account:
            return None

        return convert_db_model_to_account_dto(account)

    @exception_mapper
    async def get_users_with_role(self, user_role: UserRole) -> list[UserDTO]:
        """Получаем пользователей с ролью"""

        query = select(User).where(User.role == user_role)

        res = await self._session.scalars(query)
        users: list[User] = list(res)

        return [convert_db_model_to_user_dto(user) for user in users]

    @exception_mapper
    async def get_user_by_account_account_id(self, account_id: int) -> UserDTO | None:
        """Получаем пользователя по номеру аккаунта"""

        user: User | None = await self._session.scalar(
            select(User).join(User.accounts).where(Account.account_id == account_id)
        )
        if not user:
            return None

        return convert_db_model_to_user_dto(user)


class UserRepoImpl(SQLAlchemyRepo, UserRepo):
    @exception_mapper
    async def acquire_user_by_id(self, user_id: int) -> UserEntity:
        """Получаем пользователя по id"""

        user: User | None = await self._session.scalar(
            select(User)
            .where(
                User.id == user_id,
            )
            .options(joinedload(User.referrals))
            .with_for_update()
        )
        if not user:
            raise UserIdNotExist

        return convert_db_model_to_user_entity(user)

    @exception_mapper
    async def acquire_user_by_tg_id(self, user_tg_id: int) -> UserEntity:
        """Получаем пользователя по telegram id"""

        user: User | None = await self._session.scalar(
            select(User)
            .where(
                User.telegram_id == user_tg_id,
            )
            .options(joinedload(User.referrals))
        )
        if not user:
            raise UserTgIdNotExist

        return convert_db_model_to_user_entity(user)

    @exception_mapper
    async def create_user(self, user: UserEntity) -> None:
        db_user = convert_user_entity_to_db_model(user)
        self._session.add(db_user)
        try:
            await self._session.flush((db_user,))
        except IntegrityError as err:
            self._parse_error(err)

    @exception_mapper
    async def create_user_account(self, account: AccountEntity) -> AccountEntity:
        db_account = convert_account_entity_to_db_model(account)
        self._session.add(db_account)
        try:
            await self._session.flush((db_account,))
        except IntegrityError as err:
            self._parse_error(err)

        return convert_db_model_to_account_entity(db_account)

    @exception_mapper
    async def update_user(self, user: UserEntity) -> None:
        db_user = convert_user_entity_to_db_model(user)
        try:
            await self._session.merge(db_user)
        except IntegrityError as err:
            self._parse_error(err)

    @staticmethod
    def _parse_error(err: DBAPIError) -> None:
        error_msg = err.__cause__.args[0]

        if "pk_user" in error_msg:
            raise UserIdAlreadyExist from err
        elif "uq_user_telegram_id" in error_msg:
            raise UserTgIdAlreadyExist from err
        else:
            raise RepoError from err
